// Copyright (c) 2015 David Muse
// See the COPYING file for more information.

#include <wikitext/private/dll.h>
#include <rudiments/compiler.h>
#include <rudiments/file.h>
#include <rudiments/permissions.h>
#include <rudiments/process.h>

struct nameswap {
	const char	*oldname;
	const char	*newname;
};

class WIKITEXT_DLLSPEC xhtml_processor : public compilerprocessor {
	public:
			xhtml_processor();
		bool	process(xmldomnode *tree,
				xmldomnode *metadata);
	private:
		xmldomnode	*wrapDefinitions(xmldomnode *node);
		xmldomnode	*wrapList(xmldomnode *node);
		xmldomnode	*wrapTable(xmldomnode *node);
		xmldomnode	*wrapReplies(xmldomnode *node);

		void		handleHorizontalRule(xmldomnode *node);
		void		handleHeading(xmldomnode *node);
		void		handleAnchorName(xmldomnode *node);
		void		handleAnchorLink(xmldomnode *node);
		xmldomnode	*handleImage(xmldomnode *node);
		void		handleInternalLink(xmldomnode *node);
		void		handleExternalLink(xmldomnode *node);
		void		colorize(const char *code,
					stringbuffer *colorizedcode);

		void		buildTextFromNodeAndDescendents(
							xmldomnode *start,
							xmldomnode *top,
							stringbuffer *output);
		
};

static const char *unwrap[]={
	"doc",
	"esc",
	"code",
	NULL
};

static nameswap	ns[]={
	{"ds","br"},
	{"opt_ws","ws"},
	{"opt_ws_en","ws"},
	{"cc_txt","txt"},
	{"pre_txt","txt"},
	{"code_txt","txt"},
	{"proto","txt"},
	{"bq","blockquote"},
	{NULL,NULL}
};

xhtml_processor::xhtml_processor() {
}

bool xhtml_processor::process(xmldomnode *tree, xmldomnode *metadata) {

	xmldomnode *child=tree->getFirstTagChild();
	while (!child->isNullNode()) {

		// get next in case we rebuild the tree a bit
		xmldomnode	*next=child->getNextTagSibling();

		// get the node namespace and name
		const char	*nodens=child->getNamespace();
		const char	*nodename=child->getName();

		// handle definitions before unwrapping
		if (!charstring::compare(nodename,"def")) {
			child=wrapDefinitions(child);
			continue;
		}

		// unwrap
		bool	unwrapped=false;
		for (const char * const *u=unwrap; *u; u++) {
			if (!charstring::compare(nodename,*u)) {
				uint64_t	pos=child->getPosition();
				tree->unwrapChild(child);
				child=tree->getChild(pos);
				unwrapped=true;
				break;
			}
		}
		if (unwrapped) {
			continue;
		}

		// do some specific translations
		if (!charstring::compare(nodename,"hr")) {
			handleHorizontalRule(child);
		} else if (nodename[0]=='h' &&
				charstring::isNumber(nodename+1)) {
			handleHeading(child);
		} else if (!charstring::compare(nodename,"a_name")) {
			handleAnchorName(child);
			continue;
		} else if (!charstring::compare(nodename,"a_link")) {
			handleAnchorLink(child);
		} else if (!charstring::compare(nodens,"wt") &&
				!charstring::compare(nodename,"img")) {
			child=handleImage(child);
			continue;
		} else if (!charstring::compare(nodename,"i_link")) {
			handleInternalLink(child);
		} else if (!charstring::compare(nodename,"e_link")) {
			handleExternalLink(child);
		} else if ((!charstring::compare(nodename,"ul",2) ||
				!charstring::compare(nodename,"ol",2) ||
				!charstring::compare(nodename,"il",2) ||
				!charstring::compare(nodename,"al",2)) &&
				charstring::toInteger(nodename+2)) {
			child=wrapList(child);
			continue;
		} else if (!charstring::compare(nodens,"wt") &&
				(!charstring::compare(nodename,"th") ||
				!charstring::compare(nodename,"td"))) {
			child=wrapTable(child);
			continue;
		} else if (nodename[0]=='r' &&
				charstring::isNumber(nodename+1) &&
				charstring::compare(
					child->getParent()->getName(),
					"p")) {
			child=wrapReplies(child);
			continue;
		} else if (!charstring::compare(nodens,"wt") &&
				!charstring::compare(nodename,"code_txt")) {

			stringbuffer	colorizedcode;
			colorize(child->getAttributeValue("value"),
							&colorizedcode);
			child->setAttributeValue("value",
						colorizedcode.getString());
		}

		// rename the node
		for (nameswap *n=ns; n->oldname; n++) {
			if (!charstring::compare(nodename,n->oldname)) {
				child->setName(n->newname);
			}
		}

		// descend
		if (!process(child,metadata)) {
			return false;
		}

		// move on
		child=next;
	}

	return true;
}

xmldomnode *xhtml_processor::wrapDefinitions(xmldomnode *child) {

	// wrap runs, renaming and cleaning up as we go...

	xmldomnode	*start=child;
	xmldomnode	*end=child;
	for (;;) {
		end->deleteChildren("opt_ws");
		if (!charstring::compare(
				end->getNextTagSibling()->getName(),
				"def")) {
			end=end->getNextTagSibling();
		} else {
			break;
		}
	}

	xmldomnode	*dl=start->getParent()->
				wrapChildren(start,end,"xhtml","dl");
	dl->unwrapChildren("def");
	return dl;
}

xmldomnode *xhtml_processor::wrapList(xmldomnode *child) {

	// wrap runs, renaming as we go...

	const char	*nodename=child->getName();
	char		*nodeprefix=charstring::duplicate(child->getName(),2);
	uint16_t	index=charstring::toInteger(nodename+2);

	xmldomnode	*start=child;
	xmldomnode	*end=child;
	for (;;) {
		if (charstring::toInteger(end->getName()+2)==index) {
			end->setName("xhtml","li");
		}
		xmldomnode	*next=end->getNextTagSibling();
		if (!charstring::compare(next->getName(),nodeprefix,2)) {
			end=next;
		} else {
			break;
		}
	}

	return start->getParent()->wrapChildren(start,end,"xhtml",nodeprefix);
}

xmldomnode *xhtml_processor::wrapTable(xmldomnode *child) {

	// wrap runs, renaming as we go...

	for (;;) {

		// find the run of headers and cells...
		xmldomnode	*start=child;
		xmldomnode	*end=child;
		for (;;) {
			if (!charstring::compare(end->getNamespace(),"wt") &&
				(!charstring::compare(end->getName(),"th") ||
				!charstring::compare(end->getName(),"td"))) {
				end->setNamespace("xhtml");
			}
			xmldomnode	*next=end->getNextTagSibling();
			if (!charstring::compare(next->getNamespace(),"wt") &&
				(!charstring::compare(next->getName(),"th") ||
				!charstring::compare(next->getName(),"td"))) {
				end=next;
			} else {
				break;
			}
		}

		child=end->getNextTagSibling();

		// If the next child after the run is a tbr then
		// remove it.  If we find something other than a tbr
		// then we're at the end of the table.
		bool	endoftable=false;
		if (!charstring::compare(child->getName(),"tbr")) {
			xmldomnode	*next=child->getNextTagSibling();
			child->getParent()->deleteChild(child);
			child=next;
		} else {
			endoftable=true;
		}

		// wrap the run of headers/cells with a tr
		end=start->getParent()->wrapChildren(start,end,"xhtml","tr");

		// if we're at the end of the table then back up,
		// wrap the table, and return the table node
		if (endoftable) {
			start=end;
			for (;;) {
				if (!charstring::compare(
					start->getPreviousTagSibling()->
					getName(),"tr")) {
					start=start->getPreviousTagSibling();
				} else {
					break;
				}
			}
			return start->getParent()->
				wrapChildren(start,end,"xhtml","table");
		}
	}
}

xmldomnode *xhtml_processor::wrapReplies(xmldomnode *child) {

	// wrap runs...
	xmldomnode	*start=child;
	xmldomnode	*end=child;
	for (;;) {
		xmldomnode	*next=end->getNextTagSibling();
		const char	*nodename=next->getName();
		if (nodename[0]=='r' && charstring::isNumber(nodename+1)) {
			end=next;
		} else {
			break;
		}
	}

	return child->getParent()->wrapChildren(start,end,"xhtml","p");
}

void xhtml_processor::handleHorizontalRule(xmldomnode *node) {
	node->deleteChildren();
}

void xhtml_processor::handleHeading(xmldomnode *node) {

	// convert heading id to id attribute...

	xmldomnode	*headingid=node->getFirstTagChild("h_id");
	if (headingid->isNullNode()) {
		return;
	}
	node->setAttributeValue("id",headingid->getAttributeValue("value"));
	node->deleteChildren("h_id");
}

void xhtml_processor::handleAnchorName(xmldomnode *node) {

	// rename the node
	node->setName("xhtml","a");

	// use the first text child as the name and then delete it
	xmldomnode	*txt=node->getFirstTagChild("txt");
	node->setAttributeValue("name",txt->getAttributeValue("value"));
	node->deleteChild(txt);

	// delete any whitespace
	xmldomnode	*first=node->getFirstTagChild();
	if (!charstring::compare(first->getName(),"ws")) {
		node->deleteChild(first);
	}

	// move any additional nodes outside
	uint64_t	pos=node->getPosition()+1;
	for (first=node->getFirstTagChild(); !first->isNullNode();
					first=first->getFirstTagChild()) {
		node->moveChild(first,node->getParent(),pos);
		pos++;
	}
}

void xhtml_processor::handleAnchorLink(xmldomnode *node) {

	// rename the node
	node->setName("xhtml","a");

	// use the first text child as the href and then delete it
	xmldomnode	*txt=node->getFirstTagChild("txt");
	stringbuffer	href;
	href.append('#')->append(txt->getAttributeValue("value"));
	node->setAttributeValue("href",href.getString());
	node->deleteChild(txt);

	// delete any whitespace
	xmldomnode	*first=node->getFirstTagChild();
	if (!charstring::compare(first->getName(),"ws")) {
		node->deleteChild(first);
	}
}

xmldomnode *xhtml_processor::handleImage(xmldomnode *node) {

	// switch namespace
	node->setNamespace("xhtml");

	// set the src
	stringbuffer	src;
	buildTextFromNodeAndDescendents(node->getFirstTagChild(),node,&src);
	node->setAttributeValue("src",src.getString());

	// remove any children
	node->deleteChildren();

	return node;
}

void xhtml_processor::handleInternalLink(xmldomnode *node) {

	// create a url with the same value as the text
	stringbuffer	txt;
	buildTextFromNodeAndDescendents(node->getFirstTagChild(),node,&txt);
	node->insertTag("xhtml","url",0)->
			setAttributeValue("value",txt.getString());
}

void xhtml_processor::handleExternalLink(xmldomnode *node) {

	xmldomnode	*url=node->getFirstTagChild("url");

	// handle urls that should have a blank target
	if (charstring::contains(url->getAttributeValue("value"),"://")) {
		url->setName("exturl");
	}

	// remove leading whitespace from text
	xmldomnode	*urlsibling=url->getNextTagSibling();
	if (!charstring::compare(urlsibling->getName(),"ws")) {
		xmldomnode	*next=urlsibling->getNextTagSibling();
		node->deleteChild(urlsibling);
		urlsibling=next;
	}

	// if there's no text, then use the url as text
	if (urlsibling->isNullNode()) {
		stringbuffer	txt;
		buildTextFromNodeAndDescendents(url,node,&txt);
		node->appendTag("xhtml","txt")->
			setAttributeValue("value",txt.getString());
	}
}

void xhtml_processor::colorize(const char *code,
				stringbuffer *colorizedcode) {

	// FIXME: this is woefully inefficient

	// create a temporary input file
	char	intmpfilename[12]="/tmp/XXXXXX";
	int32_t	intfd=file::createTemporaryFile(intmpfilename,
				permissions::evalPermString("rw-------"));
	if (intfd==-1) {
		return;
	}

	// create a temporary output file
	char	outtmpfilename[12]="/tmp/XXXXXX";
	int32_t	outfd=file::createTemporaryFile(outtmpfilename,
				permissions::evalPermString("rw-------"));
	if (outfd==-1) {
		return;
	}

	// write the code to the input file
	file	intf;
	intf.setFileDescriptor(intfd);
	if (intf.write(code,charstring::length(code))!=
				charstring::length(code)) {
		intf.close();
		file::remove(intmpfilename);
		return;
	}
	intf.close();

	// colorize the input file
	stringbuffer	colorcmd;
	colorcmd.append("source-highlight");
	colorcmd.append(" --infer-lang -s C");
	colorcmd.append(" -i ");
	colorcmd.append(intmpfilename);
	colorcmd.append(" -o ");
	colorcmd.append(outtmpfilename);
	colorcmd.append(" 2> /dev/null ");

	const char	*args[100];
	uint32_t	a=0;
	args[a++]="sh";
	args[a++]="-c";
	args[a++]=colorcmd.getString();
	args[a++]=NULL;
	pid_t	pid=process::spawn("/bin/sh",args,false);
	if (pid!=-1) {
		process::wait(pid);
	}


	// get the colorized code
	char	*ccode=file::getContents(outtmpfilename);

	// get rid of any trailing carriage returns
	char	*tt=charstring::findFirst(ccode,"\n</tt></pre>");
	if (tt) {
		charstring::copy(tt,"</tt></pre>\n",12);
	}

	// output the colorized code
	colorizedcode->append(ccode);
	delete[] ccode;

	// clean up
	file::remove(intmpfilename);
	file::remove(outtmpfilename);
}

void xhtml_processor::buildTextFromNodeAndDescendents(xmldomnode *node,
							xmldomnode *top,
							stringbuffer *output) {
	for (xmldomnode *desc=node; !desc->isNullNode();
					desc=desc->getNextTag(top)) {
		output->append(desc->getAttributeValue("value"));
	}
}

extern "C" {
	WIKITEXT_DLLSPEC compilerprocessor *new_xhtml_processor() {
		return new xhtml_processor();
	}
}


class WIKITEXT_DLLSPEC xhtml_postprocessor : public compilerpostprocessor {
	public:
			xhtml_postprocessor();
		bool	process(const char *input,
				stringbuffer *output,
				xmldomnode *metadata);
};

xhtml_postprocessor::xhtml_postprocessor() {
}

static const char *headerstarts[]={
	"<h1>",
	"<h2>",
	"<h3>",
	"<h4>",
	"<h5>",
	"<h6>",
	NULL
};

static const char *headerends[]={
	"</h1>",
	"</h2>",
	"</h3>",
	"</h4>",
	"</h5>",
	"</h6>",
	NULL
};

bool xhtml_postprocessor::process(const char *input,
					stringbuffer *output,
					xmldomnode *metadata) {

	// currently, we support parameters:
	//
	// <xhtml page="yes" style="css/styles.html"/>

	bool		page=false;
	const char	*style=NULL;

	if (parameters) {
		page=!charstring::compare(
			parameters->getAttributeValue("page"),"yes");
		style=parameters->getAttributeValue("style");
	}

	// prepend an html page header...
	if (page) {

		output->append(
			"<html>\n"
			"<head>\n");

		// find the title from the first header and insert it...
		for (uint16_t index=0; headerstarts[index]; index++) {

			const char	*hstart=charstring::findFirst(
						input,headerstarts[index]);
			if (!hstart) {
				continue;
			}

			const char	*hend=charstring::findFirst(
						hstart,headerends[index]);
			if (!hend) {
				continue;
			}

			char	*title=charstring::duplicate(
						hstart+4,hend-(hstart+4));
			if (!charstring::isNullOrEmpty(title)) {
				output->append("	<title>")->
					append(title)->
					append("</title>\n");
			}
			delete[] title;
			break;
		}

		// insert the stylesheet
		if (!charstring::isNullOrEmpty(style)) {
			output->append(
				"	<link rel=\"stylesheet\" href=\"");
			output->append(style);
			output->append("\">\n");
		}

		output->append(
			"</head>\n"
			"<body>\n");
	}

	output->append(input);

	// append an html page footer...
	if (page) {
		output->append(
			"</body>\n"
			"</html>\n");
	}
	
	return true;
}

extern "C" {
	WIKITEXT_DLLSPEC compilerpostprocessor *new_xhtml_postprocessor() {
		return new xhtml_postprocessor();
	}
}
